jQuery(function ($) {

    if (!$.pubsub) $.pubsub = {};
    else if (typeof ($.pubsub) != "object") {
        throw new Error("Cannot create jQuery.pubsub namespace: it already exists and is not an object.");
    }

    // TODO: default base address to origin server
    $.pubsub.baseAddress = "/ps/memory/";

    $.pubsub._Subscription = function (args /* .topicId, [.from], [.onMessageReceived], [.onError] */) {
        if (!args || !args.topicId) {
            throw new Error("_Subscription constructor requires an object with 'topicId' property set to a positive integer.");
        }
        this.topicId = args.topicId;
        if (args.from) this.from = args.from;
        if (args.onMessageReceived) this.onMessageReceived = args.onMessageReceived;
        if (args.onError) this.onError = args.onError;
    }

    $.pubsub._Subscription._subs = {};
    $.pubsub._Subscription._subsCount = 0;

    abortCurrentPoll = function (restart) {
        if ($.pubsub._Subscription._poll) {
            $.pubsub._Subscription._poll.explicitlyAborted = 1;
            $.pubsub._Subscription._poll.abort();
        }
        else if (restart) {
            ensurePolling();
        }
    }

    removeSubscription = function (topicId) {
        if ($.pubsub._Subscription._subs[topicId]) {
            delete $.pubsub._Subscription._subs[topicId];
            $.pubsub._Subscription._subsCount--;
        }
    }

    encodeSubscriptionParameters = function () {
        var requestParams = '';
        var i = 0;
        for (var topicId in $.pubsub._Subscription._subs) {
            var sub = $.pubsub._Subscription._subs[topicId];
            if (i > 0)
                requestParams += '&';
            requestParams += 'subs[' + i + '][topicid]=' + topicId;
            if (sub.from)
                requestParams += '&subs[' + i + '][from]=' + sub.from;
            i++;
        }
        return requestParams;
    }

    processMime = function (x) {
        try {
            if (!$.pubsub._Subscription._mimeBoundary) {
                var contenttype = x.getResponseHeader('Content-type');
                if (!contenttype)
                    throw new Error("Content-type header is missing from the HTTP long poll response.")
                var result = contenttype.match(/multipart\/mixed\s*;\s*boundary\=([\w\.]*)$/i);
                if (!result || result.length != 2)
                    throw new Error("Unable to determine multipart/mixed boundary based on the content-type header of the HTTP long poll response.")
                $.pubsub._Subscription._mimeBoundary = '--' + result[1];
                $.pubsub._Subscription._mimePartIndex = 0;
            }

            if (x.responseText) {
                while (x.responseText.length > $.pubsub._Subscription._mimePartIndex) {
                    var boundaryIndex = x.responseText.indexOf($.pubsub._Subscription._mimeBoundary, $.pubsub._Subscription._mimePartIndex);
                    if (boundaryIndex == -1)
                        break;
                    if ($.pubsub._Subscription._mimePartIndex == 0)
                        $.pubsub._Subscription._mimePartIndex = boundaryIndex + $.pubsub._Subscription._mimeBoundary.length + 2;
                    else {
                        var mimePart = x.responseText.substring($.pubsub._Subscription._mimePartIndex, boundaryIndex - 2);
                        processSingleMimePart(mimePart);
                        $.pubsub._Subscription._mimePartIndex = boundaryIndex + $.pubsub._Subscription._mimeBoundary.length + 2;
                    }
                }
            }
        }
        catch (e) {
            // TODO: consider interaction with callbacks; should we abort the http request?
            faultAllSubscriptions(x, "HTTP long poll response is malformed. All subscriptions are faulted.");
            x.abort();
        }
    }

    processSingleMimePart = function (mimePart) {
        var message = {};
        var match = mimePart.match(/Content-type:\s*([^\r]+)\r\n/i);
        if (!match || match.length != 2)
            throw new Error("Cannot determine the content type of the MIME part.");
        message.contentType = match[1];
        match = mimePart.match(/Content-Description:\s*(\d+)\/(\d+)\r\n/i);
        if (!match || match.length != 3)
            throw new Error("Cannot determine topicId or messageId of the MIME part.");
        message.topicId = match[1] - 0;
        message.messageId = match[2] - 0;
        message.body = mimePart.slice(mimePart.indexOf("\r\n\r\n") + "\r\n\r\n".length);
        dispatchMessage(message);
    }

    dispatchMessage = function (message) {
        if ($.pubsub._Subscription._subs[message.topicId]) {
            var sub = $.pubsub._Subscription._subs[message.topicId];
            if (!sub.from || sub.from <= message.messageId)
                sub.from = message.messageId + 1;
            try {
                if (sub.onMessageReceived)
                    sub.onMessageReceived(message);
            }
            catch (e) { }
        }
    }

    faultAllSubscriptions = function (request, message) {
        var event = { httpRequest: request, error: message };
        var subsToFault = {};
        for (var topicId in $.pubsub._Subscription._subs)
            subsToFault[topicId] = $.pubsub._Subscription._subs[topicId];
        for (topicId in subsToFault) {
            var sub = $.pubsub._Subscription._subs[topicId];
            removeSubscription(topicId);
            try {
                if (sub.onError)
                    sub.onError(event);
            }
            catch (e) { }
        }
    }

    ensurePolling = function () {
        if (!$.pubsub._Subscription._poll && $.pubsub._Subscription._subsCount > 0) {
            delete $.pubsub._Subscription._mimePartIndex;
            delete $.pubsub._Subscription._mimeBoundary;
            $.ajax({
                url: $.pubsub.baseAddress + "subscriptions/volatile?" + encodeSubscriptionParameters(),
                processData: false,
                beforeSend: function (x) {
                    $.pubsub._Subscription._poll = x;
                },
                success: function (d, s, x) {
                    if (x.explicitlyAborted) {
                        delete $.pubsub._Subscription._poll;
                        ensurePolling();
                    }
                    else {
                        var contenttype = x.getResponseHeader('Content-type');
                        if (contenttype && contenttype.match(/multipart\/mixed/)) {
                            processMime(x);
                        }
                        delete $.pubsub._Subscription._poll;
                        ensurePolling();
                    }
                },
                error: function (x, s, e) {
                    faultAllSubscriptions(x, "HTTP long poll has failed. All subscriptions are faulted.");
                }
            });
        }
    }

    $.pubsub.createTopic = function (args /* [.onSuccess], [.onError] */) {
        $.ajax({
            url: $.pubsub.baseAddress + "topics",
            type: "POST",
            dataType: "text",
            success: args && args.onSuccess ? function (d, s, x) {
                args.onSuccess({ args: args, topicId: d - 0, httpRequest: x });
            } : null,
            error: args && args.onError ? function (x, s, e) {
                args.onError({ args: args, httpRequest: x });
            } : null
        });
    }

    $.pubsub.publish = function (args /* .topicId, [.contentType], .body, [.onSuccess], [.onError] */) {
        if (!args || !args.topicId || !args.body) {
            throw new Error("Publish method requires an object with 'topicId' (integer) and 'body' (string) properties.");
        }
        $.ajax({
            url: $.pubsub.baseAddress + "topics/" + args.topicId,
            type: "POST",
            contentType: args.contentType ? args.contentType : "text/plain",
            data: args.body,
            dataType: "text",
            processData: false,
            success: args.onSuccess ? function (d, s, x) {
                args.onSuccess({ args: args, messageId: d - 0, httpRequest: x });
            } : null,
            error: args.onError ? function (x, s, e) {
                args.onError({ args: args, httpRequest: x });
            } : null
        });
    }

    $.pubsub.subscribe = function (args) {
        var sub = new $.pubsub._Subscription(args);
        if ($.pubsub._Subscription._subs[sub.topicId])
            throw new Error("Cannot start subscription. Only one subscription to a given topic may be started at a time.");
        $.pubsub._Subscription._subs[sub.topicId] = sub;
        $.pubsub._Subscription._subsCount++;
        abortCurrentPoll(true);
    }

    $.pubsub.unsubscribe = function (topicId) {
        removeSubscription(topicId);
        if ($.pubsub._Subscription.subsCount == 0) {
            abortCurrentPoll(false);
        }
    }

    $.pubsub.unsubscribeAll = function () {
        var subsToRemove = {};
        for (var topicId in $.pubsub._Subscription._subs) {
            subsToRemove[topicId] = 1;
        }
        for (topicId in subsToRemove) {
            removeSubscription(topicId);
        }
        abortCurrentPoll(false);
    }

});